﻿// Copyright © Microsoft Corporation.  All Rights Reserved.
// This code released under the terms of the 
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)

namespace Expression.Samples.PathListBoxUtils
{
	using System;
	using System.Windows;
	using System.Windows.Controls;
	using System.Windows.Input;
	using System.Windows.Interactivity;
	using System.Windows.Media.Animation;
	using Microsoft.Expression.Controls;
	using Microsoft.Expression.Interactivity.Core;

	public class PathListBoxScrollBehavior : Behavior<ListBox>
	{
		// We cannot type constrain to PathListBox due to a bug in Blend.  A Blend crash can occur if a behavior constrains 
		// to a type that is in an assembly that was not loaded when the Asset panel was first opened.
		// public class PathListBoxScrollBehavior : Behavior<PathListBox>
	
		#region Private Fields
		private PathListBoxItem enteringItem;
		private int newIndex = -1;
		private Storyboard startPropertyStoryboard;
		#endregion

		#region DependencyProperty Amount
		/// <summary>
		/// Gets or sets the amount to increment or decrement the StartItemIndex.
		/// </summary>
		public int Amount
		{
			get { return (int)this.GetValue(PathListBoxScrollBehavior.AmountProperty); }
			set { this.SetValue(PathListBoxScrollBehavior.AmountProperty, value); }
		}

		public static readonly DependencyProperty AmountProperty =
			DependencyProperty.Register("Amount", typeof(int), typeof(PathListBoxScrollBehavior), new PropertyMetadata(1));
		#endregion

		#region DependencyProperty DesiredOffset
		/// <summary>
		/// Gets or sets the offset from 0 to 1 along the layout paths associated with this PathListBox to scroll the selected item.
		/// </summary>
		public Double DesiredOffset
		{
			get { return (Double)this.GetValue(PathListBoxScrollBehavior.DesiredOffsetProperty); }
			set { this.SetValue(PathListBoxScrollBehavior.DesiredOffsetProperty, value); }
		}

		public static readonly DependencyProperty DesiredOffsetProperty =
			DependencyProperty.Register("DesiredOffset", typeof(Double), typeof(PathListBoxScrollBehavior), new PropertyMetadata(1.0));
		#endregion

		#region DependencyProperty Duration
		/// <summary>
		/// Gets or sets the duration of the scroll.
		/// </summary>
		public Duration Duration
		{
			get { return (Duration)this.GetValue(PathListBoxScrollBehavior.DurationProperty); }
			set { this.SetValue(PathListBoxScrollBehavior.DurationProperty, value); }
		}

		public static readonly DependencyProperty DurationProperty =
			DependencyProperty.Register("Duration", typeof(Duration), typeof(PathListBoxScrollBehavior), new PropertyMetadata(new Duration(new TimeSpan(0, 0, 1))));
		#endregion

		#region DependencyProperty Ease
		/// <summary>
		/// Gets or sets the easing function to use when scrolling.
		/// </summary>
		public EasingFunctionBase Ease
		{
			get { return (EasingFunctionBase)this.GetValue(PathListBoxScrollBehavior.EaseProperty); }
			set { this.SetValue(PathListBoxScrollBehavior.EaseProperty, value); }
		}

		public static readonly DependencyProperty EaseProperty =
			DependencyProperty.Register("Ease", typeof(EasingFunctionBase), typeof(PathListBoxScrollBehavior), new PropertyMetadata(null));
		#endregion

		#region DependencyProperty HideEnteringItem
		/// <summary>
		/// Gets or sets whether to hide the item that will be newly arranged on the path when the StartItemIndex is changed. Use this when scrolling on an open path.
		/// </summary>
		public bool HideEnteringItem
		{
			get { return (bool)this.GetValue(PathListBoxScrollBehavior.HideEnteringItemProperty); }
			set { this.SetValue(PathListBoxScrollBehavior.HideEnteringItemProperty, value); }
		}

		public static readonly DependencyProperty HideEnteringItemProperty =
			DependencyProperty.Register("HideEnteringItem", typeof(bool), typeof(PathListBoxScrollBehavior), new PropertyMetadata(false));
		#endregion

		/// <summary>
		/// Initializes a new instance of the PathListBoxScrollBehavior class.
		/// </summary>
		public PathListBoxScrollBehavior()
		{
			this.IncrementCommand = new ActionCommand(this.Increment);
			this.DecrementCommand = new ActionCommand(this.Decrement);
			this.ScrollSelectedCommand = new ActionCommand(this.ScrollSelected);

			this.startPropertyStoryboard = new Storyboard();
			this.startPropertyStoryboard.Completed += this.StartPropertyStoryboard_Completed;
		}

		/// <summary>
		/// Gets a command that increments the StartItemIndex by Amount.
		/// </summary>
		public ICommand	IncrementCommand
		{
			get;
			private set;
		}
		
		/// <summary>
		/// Gets a command that decrements the StartItemIndex by Amount.
		/// </summary>
		public ICommand DecrementCommand
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets a command that scrolls the selected item to the same position as the item that is closest to the DesiredOffset.
		/// </summary>
		public ICommand ScrollSelectedCommand
		{
			get;
			private set;
		}

		#region Private Methods
		private void ScrollSelected()
		{
			PathListBox pathListBox = this.AssociatedObject as PathListBox;
			if (pathListBox == null)
			{
				return;
			}
			PathListBoxItem newItem = (PathListBoxItem) pathListBox.ItemContainerGenerator.ContainerFromItem(pathListBox.SelectedItem);
			
			// find the item on the path that is closest to the position
			PathListBoxItem closestItem = null;
			PathListBoxItem pathListBoxItem;
			for (int i = 0; i < pathListBox.Items.Count; i++)
			{
				pathListBoxItem = (PathListBoxItem)pathListBox.ItemContainerGenerator.ContainerFromIndex(i);
				if (pathListBoxItem != null && pathListBoxItem.IsArranged)
				{
					if (closestItem == null)
					{
						closestItem = pathListBoxItem;
					}
					else if (Math.Abs(pathListBoxItem.LocalOffset - this.DesiredOffset) < Math.Abs(closestItem.LocalOffset - this.DesiredOffset))
					{
						closestItem = pathListBoxItem;
					}
				}
			}
			
			if (closestItem == null || newItem == null || !newItem.IsArranged || !closestItem.IsArranged)
			{
				return;
			}
			int increment = 0;

			if (newItem.GlobalOffset < closestItem.GlobalOffset && newItem.GlobalIndex > closestItem.GlobalIndex)
			{
				increment = -(pathListBox.Items.Count - newItem.GlobalIndex + closestItem.GlobalIndex);
			}
			else if (newItem.GlobalOffset > closestItem.GlobalOffset && newItem.GlobalIndex < closestItem.GlobalIndex)
			{
				increment = (pathListBox.Items.Count - closestItem.GlobalIndex + newItem.GlobalIndex);
			}
			else
			{
				increment = newItem.GlobalIndex - closestItem.GlobalIndex;
			}

			bool hideEnteringItem = this.HideEnteringItem;
			this.HideEnteringItem = false;

			Scroll(increment);

			this.HideEnteringItem = hideEnteringItem;

		}

		private void Increment()
		{
			Scroll(this.Amount);
		}

		private void Decrement()
		{
			Scroll(-this.Amount);
		}

		private void Scroll(int increment)
		{
			bool hideEnteringItem = this.HideEnteringItem;
			if (Math.Abs(increment) > 1)
			{
				this.HideEnteringItem = false;
			}

			PathListBox pathListBox = this.AssociatedObject as PathListBox;
			if (pathListBox == null || pathListBox.Items == null || pathListBox.Items.Count <= 1)
			{
				return;
			}

			if (pathListBox.StartItemIndex >= pathListBox.Items.Count)
			{
				throw new ArgumentOutOfRangeException("StartItemIndex", "Value may be greater than the number of items in the collection.");
			}

			int startItemIndex = Math.Max(0, (int)Math.Round(pathListBox.StartItemIndex));
			int itemCount = pathListBox.Items.Count;

			// We have interrupted a storyboard already in progress, so save off what the 
			// startItemIndex would have been if the storyboard completed and increment
			// the this.newIndex.
			if (this.newIndex != -1)
			{
				startItemIndex = this.newIndex;
			}

			int newIndex = (startItemIndex + increment) % itemCount;
			if (newIndex < 0)
			{
				if (pathListBox.WrapItems)
				{
					newIndex += itemCount;
				}
				else
				{
					return;
				}
			}
			this.newIndex = newIndex;

			// If the storyboard is playing interupt it.
			TimeSpan timeRemaining = new TimeSpan();
			if (IsStoryboardPlaying())
			{
				timeRemaining = this.startPropertyStoryboard.GetCurrentTime();
				this.startPropertyStoryboard.Pause();
			}
			else
			{
				this.startPropertyStoryboard.Stop();
				this.startPropertyStoryboard.Children.Clear();
			}

			// Compute the new duration based on whatever is remaining plus what the user specified.
			Duration newDuration = new Duration(timeRemaining);
			if (this.Duration == Duration.Automatic)
			{
				// if the duration is automatic it is 1 second
				newDuration += new TimeSpan(0, 0, 1);
			}
			else
			{
				newDuration += this.Duration;
			}

			for (int i = 0; i < pathListBox.LayoutPaths.Count; i++)
			{
				this.AnimateStart(pathListBox, increment, i, startItemIndex, newDuration, !TimeSpan.Equals(timeRemaining, new TimeSpan()));
			}

			this.UpdateStartItemIndex(pathListBox);

			this.startPropertyStoryboard.Begin();

			this.HideEnteringItem = hideEnteringItem;
		}

		private void UpdateStartItemIndex(PathListBox pathListBox)
		{
			pathListBox.StartItemIndex = this.newIndex;
			this.newIndex = -1;
		}

		private void ShowItems()
		{
			if (this.enteringItem != null)
			{
				this.enteringItem.Opacity = 1;
				this.enteringItem = null;
			}
		}

		private void StartPropertyStoryboard_Completed(object sender, EventArgs e)
		{
			ShowItems();
		}

		private void AnimateStart(PathListBox pathListBox, int increment, int layoutPathIndex, int startItemIndex, Duration newDuration, bool isStoryboardInterruped)
		{
			LayoutPath layoutPath = pathListBox.LayoutPaths[layoutPathIndex];
			if (layoutPath == null || increment == 0)
			{
				return;
			}

			// Get the distance between the two elements that are being arranged to compute the distance to animate start.
			int itemCount = pathListBox.Items.Count;

			int firstIndex = 0;
			int nextIndex = 0;
			
			firstIndex = PathListBoxExtensions.GetFirstArrangedIndex(pathListBox, layoutPathIndex);
			nextIndex = (firstIndex + 1) % itemCount;

			PathListBoxItem plbiStart = (PathListBoxItem)(pathListBox.ItemContainerGenerator.ContainerFromIndex(firstIndex));
			PathListBoxItem plbiEnd = (PathListBoxItem)(pathListBox.ItemContainerGenerator.ContainerFromIndex(nextIndex));

			if (plbiStart.IsArranged == false || plbiEnd.IsArranged == false)
			{
				if (pathListBox.WrapItems)
				{
					while (plbiStart.IsArranged == false || plbiEnd.IsArranged == false)
					{
						firstIndex = (firstIndex + 1) % itemCount;
						nextIndex = (nextIndex + 1) % itemCount;
						plbiStart = (PathListBoxItem)(pathListBox.ItemContainerGenerator.ContainerFromIndex(firstIndex));
						plbiEnd = (PathListBoxItem)(pathListBox.ItemContainerGenerator.ContainerFromIndex(nextIndex));
					}
				}
				else
				{
					return;
				}
			}

			double startDx = 0;
			if (plbiStart.LocalOffset > plbiEnd.LocalOffset)
			{
				// if the Start and End cross 0   
				startDx = increment * ((1 - plbiStart.LocalOffset) + plbiEnd.LocalOffset);
			}
			else
			{
				startDx = increment * (plbiEnd.LocalOffset - plbiStart.LocalOffset);
			}

			double startFrom = layoutPath.Start;
			double startTo = layoutPath.Start;

			PropertyPath propertyPath = new PropertyPath(String.Format("(ec:PathListBox.LayoutPaths)[{0}].(ec:LayoutPath.Start)", layoutPathIndex));

			DoubleAnimation startPropertyAnimation = null;
			if (isStoryboardInterruped)
			{
				// Find the DoubleAnimation that has already been created for this LayoutPath
				for (int i = 0; i < this.startPropertyStoryboard.Children.Count; i++)
				{
					DoubleAnimation currentAnimation = (DoubleAnimation)this.startPropertyStoryboard.Children[i];
					PropertyPath pp = Storyboard.GetTargetProperty(currentAnimation);
					if (currentAnimation != null && String.Equals(pp.Path, propertyPath.Path))
					{
						startPropertyAnimation = currentAnimation;
						break;
					}
				}
				startTo = (double)startPropertyAnimation.To;
				ShowItems();
				startFrom = startTo;
			}
			else
			{
				startPropertyAnimation = new DoubleAnimation();
				Storyboard.SetTarget(startPropertyAnimation, pathListBox);
				Storyboard.SetTargetProperty(startPropertyAnimation, propertyPath);
				this.startPropertyStoryboard.Children.Add(startPropertyAnimation);
			}

			startFrom += startDx;

			int EnteringItemIndex = 0;
			if (increment > 0)
			{
				EnteringItemIndex = pathListBox.GetLastArrangedIndex(layoutPathIndex);
				EnteringItemIndex = (EnteringItemIndex + 1) % itemCount;
			}
			else
			{
				EnteringItemIndex = (pathListBox.GetFirstArrangedIndex(layoutPathIndex) - 1) % itemCount;
				if (EnteringItemIndex < 0)
				{
					if (pathListBox.WrapItems)
					{
						EnteringItemIndex += itemCount;
					}
				}
			}
			PathListBoxItem pathListBoxItem = (PathListBoxItem)pathListBox.ItemContainerGenerator.ContainerFromIndex(EnteringItemIndex);
			if (pathListBoxItem != null && this.HideEnteringItem)
			{
				this.enteringItem = pathListBoxItem;
				this.enteringItem.Opacity = 0;
			}

			startPropertyAnimation.Duration = newDuration;
			startPropertyAnimation.From = startFrom + 0.0001;
			startPropertyAnimation.To = startTo;
			startPropertyAnimation.EasingFunction = this.Ease;
		}

		private bool IsStoryboardPlaying()
		{
			if (this.startPropertyStoryboard.Children.Count <= 0)
			{
				return false;
			}
			return (this.startPropertyStoryboard.GetCurrentState() == ClockState.Active);
		}

		#endregion
	}
}